# Copyright (c) 2009 Chuck Thier
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import time
import traceback
import urllib
import types
import datetime

from restish import http, resource, error
import simplejson
from decorator import decorator
import tempita

from resttools.lib.odict import OrderedDict

HTML_CONTENT_TYPE = "text/html; charset=utf-8"
JSON_CONTENT_TYPE = "application/json"

class Publishers(object):
    """Class to map python objects to publishers"""
    def __init__(self):
        self.publishers = {}

    def set_publisher(self, type_, publisher):
        """Set a publisher for a given type

           :param type_: python type of the object to handle
           :param publisher: class to handle the given type
        """
        self.publishers[type_] = publisher

    def get_publisher(self, type_, default=None):
        """Get a publisher for a given type

           :param type_: python type of the object to handle
           :param default: what publisher to use if one can not be found
           :returns: publisher class for the given object type
        """
        # Walk the mro tree, and return the first publisher found
        # NOTE 1-a: prior to Python 2.5 Exceptions where classic, or old-style, 
        #    classes so we cannot use __mro__.  (See NOTE 1-b.)
        #    (See also NOTE 1-b.)
        if type_ in self.publishers:
            return self.publishers.get(type_)
        for t in type_.__mro__[1:]:
            if t in self.publishers:
                return self.publishers.get(t)
        return default

# Module level global for all installed publishers
publishers = Publishers()

def delegate(data, **kwargs):
    """ Helper function to send data to its correct publisher.

        :param data: data to handle
        :param method (optional): If defined, will call that method of the
                            publisher class
        :param index (optional): Normally used when getting some subset of data
                           (like a specific item of a list, or a dict key)
    """
    if 'index' in kwargs:
        index = kwargs['index']
        publisher = publishers.get_publisher(
            type(data[index]), ObjectPublisher)(data, index)
    else:
        publisher = publishers.get_publisher(type(data), ObjectPublisher)(data)
    if 'method' in kwargs:
        #return getattr(publisher, kwargs['method'])(data, kwargs['request'])
        return getattr(publisher, kwargs['method'])(data, **kwargs)
    return publisher

class JSONEncoder(simplejson.JSONEncoder):
    """JSONEncoder that calls the publisher's render_json() function to determine
       what gets encoded
    """
    def __init__(self, skipkeys=False, ensure_ascii=True, check_circular=True,
            allow_nan=True, sort_keys=False, indent=None, separators=None,
            encoding='utf-8', default=None, request=None):
        self.request = request
        simplejson.JSONEncoder.__init__(self, skipkeys, ensure_ascii,
            check_circular, allow_nan, sort_keys, indent, separators, encoding,
            default)

    def default(self, obj):
        # NOTE 1-b: prior to Python 2.5 Exceptions where classic, or old-style, 
        #    classes so we cannot use type().  This explicitly passes Exception.
        #    (See also NOTE 1-a.)
        if (isinstance(obj, Exception)):
            publisher = publishers.get_publisher(Exception)(obj)
        else:
            publisher = publishers.get_publisher(type(obj))(obj)
        if publisher is not None:
            return publisher.render_json(obj, self.request)
        else:
            return simplejson.JSONEncoder.default(self, obj)

class BasePublisher(resource.Resource):
    """Base Object publisher class.  Subclass this class to define how to handle
       python objects.
    """
    # Override to change the HTML header or footer
    HTML_HEADER = "<html><head></head>\n"
    HTML_FOOTER = "</html>"

    # Override to use a differen wrapped results key
    RESULTS_KEY = "results"

    # Override to send a respones other that 200 OK
    response = staticmethod(http.ok)

    # Override to determine which HTTP methods are available for the object
    accepted_methods = ('GET', 'PUT')

    def __init__(self, data, index=None):
        if index is not None:
            self.data = data[index]
            self.parent = data
            self.index = index
        else:
            self.data = data
            self.parent = None
            self.index = None

    def get_data(self, request):
        return self.data

    def render_html(self, data, **kwargs):
        """Render an html representation of the data.  Most subclasses should
           override this function."""
        raise NotImplementedError

    def render_json(self, data, request, **kwargs):
        """Determine data to be serialized by simplejson.  Only override this
           in circumstances where simplejson doesn't natively know how to handle
           the object type for the publisher.
        """
        return self.data

    def wrap_response(self, data, request):
        """Wrap the response in a standard envelope"""
        wrapped = dict()
        if request:
            wrapped['self'] = request.url
            if 'resttools.wrapped' in request.environ:
                for k,v in request.environ['resttools.wrapped'].items():
                    wrapped[k] = v
        wrapped[self.RESULTS_KEY] = data
        return wrapped

    @resource.GET(accept='application/json')
    def json(self, request):
        """Serve the JSON representation of the data."""
        if 'GET' not in self.accepted_methods:
            return http.method_not_allowed(', '.join(self.methods))
        return self.handle_json(self.wrap_response(self.get_data(request), request), request)

    def handle_json(self, data, request):
        return self.response(
            [('Content-Type', JSON_CONTENT_TYPE)],
            simplejson.dumps(data, cls=JSONEncoder, request=request))

    @resource.GET(accept='text/html')
    def html(self, request):
        """Serve the HTML representation of the data."""
        if 'GET' not in self.accepted_methods:
            return http.method_not_allowed(', '.join(self.methods))
        return self.response(
            [('Content-Type', HTML_CONTENT_TYPE)],
            self.HTML_HEADER + delegate(
                self.wrap_response(
                    self.get_data(request), request),
                    method="render_html",
                    request=request,
                    linked=False) + self.HTML_FOOTER)

    @resource.child(resource.any)
    def children(self, request, segments):
        return delegate(self.get_data(request))

class ObjectPublisher(BasePublisher):
    def render_html(self, data, **kwargs):
        """Render an html representation of the data.  Most subclasses should
           override this function."""
        return str(data)
publishers.set_publisher(object, ObjectPublisher)

class StringPublisher(ObjectPublisher):
    """Publisher for python string objects."""
    pass
publishers.set_publisher(str, StringPublisher)

class UnicodePublisher(BasePublisher):
    """Publisher for python unicode objects.
       Note that currently, UTF-8 is assumed.
    """
    def render_html(self, data, **kwargs):
        """Render HTML representation of python unicode object."""
        return data.encode('utf-8')
publishers.set_publisher(unicode, UnicodePublisher)

class DictPublisher(BasePublisher):
    """Publisher for python dict objects."""
    def _get_key(self, key, path, linked):
        if linked:
            return '<a href="%s">%s</a>' % (
                self._get_path(key, path, linked), str(key))
        else:
            return str(key)

    def _get_path(self, key, path, linked):
        if linked:
            return path+urllib.quote(str(key))
        else:
            return path

    def render_html(self, data, request, linked=True, aslist=False, **kwargs):
        """Render HTML representation of a python dict."""
        template = tempita.Template(
            "<dl>"
            "{{for key, value  in data.items()}}"
            "{{if aslist}}"
            "<dt style='float:left;'>"
            "{{else}}"
            "<dt>"
            "{{endif}}"
            "{{get_key(key)}}</dt>"
            "<dd>{{next(key)}}</dd>"
            "{{endfor}}"
            "</dl>"
        )
        path = kwargs.get('path', request.path)
        if not path.endswith('/'):
            path += '/'
        return template.substitute(
            data = data,
            next = lambda key: delegate(data[key], method='render_html',
                request=request, path=self._get_path(key, path, linked)),
            get_key = lambda key: self._get_key(key, path, linked),
            aslist = aslist,
        )

    @resource.child('{key}')
    def item(self, request, segments, key):
        """Handle requests to keys for the dict"""
        key = urllib.unquote_plus(key)
        if key in self.data:
            return delegate(self.data, index=key)
        else:
            return None
publishers.set_publisher(dict, DictPublisher)

class ListPublisher(BasePublisher):
    """Publisher for python list objects."""
    accepted_methods = ('GET', 'PUT', 'POST')
    start = 0

    def render_html(self, data, request, **kwargs):
        # Render as an ordered dict, so that we can get consistent linking
        path = kwargs.get('path', request.path)
        dict_data = OrderedDict(
            ((count+self.start, item) for count, item in enumerate(data))
        )
        kwargs['method'] = 'render_html'
        kwargs['aslist'] = True
        return delegate(dict_data, request=request, **kwargs)

    @resource.child('{index}')
    def index(self, request, segments, index):
        """Handle requests for an item from the list"""
        try:
            index = int(index)+self.start
        except ValueError, e:
            raise http.BadRequestError()
        if index >= 0 and index < len(self.data):
            return delegate(self.data, index=index)
        else:
            return None

    @resource.POST()
    def post(self, request):
        """POST appends items to the end of the list.  ***UNTESTED!***"""
        if 'POST' not in self.accepted_methods:
            return http.method_not_allowed(', '.join(self.methods))
        data = None #TODO: Get the data
        self.data.append(data)
        return http.created(request.url+'/'+str(len(self.data)-1),
            '201 Created')
publishers.set_publisher(list, ListPublisher)

class TuplePublisher(ListPublisher):
    """Publisher for python tuple objects."""
    accepted_methods = ('GET', )
publishers.set_publisher(tuple, TuplePublisher)

# TODO: Figure out a better way to handle dates and times
class DateTimePublisher(ObjectPublisher):
    """Publisher for python datetime objects."""
    def get_data(self, request):
        return int(time.mktime(self.data.timetuple()))
publishers.set_publisher(datetime.datetime, DateTimePublisher)

class ExceptionPublisher(StringPublisher):
    """Publisher for python exception objects."""
    # Since and exception is an error, send an error response
    response = staticmethod(http.internal_server_error)

    def __init__(self, data):
        if isinstance(data, error.HTTPError):
            self.response = data.response_factory
            self.data = data.kwargs.get('body','500 Internal Server Error')
        else:
            self.data = '500 Internal Server Error'
publishers.set_publisher(Exception, ExceptionPublisher)

class FunctionPublisher(BasePublisher):
    """Publisher for python functions.  The functions are not called until
       accessed.
    """
    accepted_methods = ('GET',)

    def get_data(self, request):
        """Execute the function to get the data"""
        return self.data()

    def render_html(self, data, request, **kwargs):
        """Render HTML representation of a python function"""
        # call the function, and then delegate for the returned data
        return delegate(data(), method='render_html', request=request)

    def render_json(self, data, request, **kwargs):
        """Handle JSON serialization of a python function"""
        # call the function, and return the data to be serialized
        return data()
publishers.set_publisher(types.FunctionType, FunctionPublisher)

class ResourcePublisher(BasePublisher):
    """Publisher for restish resources."""
    def get_data(self, request):
        return self.data(request)

    def render_html(self, data, request, **kwargs):
        return delegate(data(request), method="render_html", request=request)

    def render_json(self, data, request, **kwargs):
        return data(request)

    @resource.PUT()
    def put(self, request):
        return self.data(request)
publishers.set_publisher(resource.Resource, ResourcePublisher)

class PublisherPublisher(BasePublisher):
    """Publisher for publishers."""
    def __init__(self, data, index=None):
        BasePublisher.__init__(self, data, index)
        self.data.response = lambda headers, body: self.data.data
        self.data.handle_json = lambda data, request: self.data.data

    def get_data(self, request):
        data = self.data(request)
        return data

    def render_html(self, data, request, **kwargs):
        return delegate(data(request), method="render_html",
                        request=request)

    def render_json(self, data, request, **kwargs):
        return data(request)
publishers.set_publisher(BasePublisher, PublisherPublisher)

class Publish(resource.Resource):
    """Helper class to expose python objects restfully."""
    def __init__(self, data):
        """
        :param data: Any python object to expose
        :param response (optional): Alternate response function to use
        """
        self.data = data

    @resource.GET()
    def render(self, request):
        """Render request for the data"""
        publisher = delegate(self.data)
        return publisher

    @resource.child(resource.any)
    def process(self, request, segments):
        """Handle child requests for the object"""
        publisher = delegate(self.data)
        return publisher
publishers.set_publisher(Publish, PublisherPublisher)

@decorator
def publish(func, *args, **kw):
    """Method decorator to auto expose the returned data restfully"""
    request = args[1]
    request.environ.setdefault('resttools.wrapped', OrderedDict())['doc'] = func.__doc__
    try:
        results = func(*args, **kw)
    except Exception, e:
        if isinstance(e, http.UnauthorizedError):
            # Let the unauthorized error pass through
            raise e
        # Hack to render the result as a GET
        request.method = 'GET'
        if not isinstance(e, error.HTTPError):
            request.environ['resttools.wrapped']['traceback'] = traceback.format_exc()
        return Publish(e)

    request.method = 'GET'
    return Publish(results)

class Resource(resource.Resource):
    """
    Helper class that defines the default child behavior so that descending
    a resource tree will work correctly
    """
    @resource.child(resource.any)
    def children(self, request, segments):
        """Handle child requests for the object"""
        return Publish(self), segments
